﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace HexTileBoard
{
    public class SimpleCamera
    {
        /// <summary>
        /// The possible locations of the viewport, in halves and quarters.
        /// </summary>
        public enum ViewportLocation
        {
            BottomHalf,
            BottomLeft,
            BottomRight,
            FullScreen,
            TopHalf,
            TopLeft,
            TopRight
        }

        // Components for player's camera
        private Matrix projectionMatrix;                        // projection matrix
        public Matrix ProjectionMatrix
        {
            get { return projectionMatrix; }
        }
        private Matrix viewMatrix;                              // view matrix
        public Matrix ViewMatrix
        {
            get { return viewMatrix; }
        }
        private const float fieldOfView = MathHelper.PiOver4;   // Field of view definition.
        private const float nearPlaneDistance = 1f;             // Near plane
        private const float farPlaneDistance = 720.0f;        // Far plane
        private float aspectRatio;                              // Aspect ratio
        private Vector2 screenSize;                             // Screen width and height
        public Vector2 ScreenSize                        // Set the width, height, and aspect ration
        {
            get { return screenSize; }
            set
            {
                screenSize = value;
                aspectRatio = screenSize.X / screenSize.Y;
            }
        }
        private BoundingFrustum viewFrustum;                    // The view frustum for the camera.
        public BoundingFrustum ViewFrustum
        {
            get { return viewFrustum; }
        }
        private Viewport customViewport;                        // Custom viewport, defined for each player
        public Viewport CustomViewport
        {
            get { return customViewport; }
        }

        // Components for gradual adjustment of camera towards desired target.
        private Vector3 VectorAfterLerp;                        // vector3 for lerp constraints. Lerp sometimes overshoots and jitters, so we restrict this way.
        private float lerpValue, x, y, z;                       // declare once, instead of every draw.
        private const float ClampValue = 0.025f;                // the value at which to stop lerping and simply place the position = desiredposition/target = desiredtarget.
        private const float MinLerpValue = 0.002f;              // the minimum transition speed for a lerp calculation, which if the FPS is very high, can be 0 for elapsed milliseconds.
        private const float zoomMin = 150.0f;
        private const float zoomMax = 800.0f;
        public float ZoomMax
        {
            get { return zoomMax; }
        }

        // Variables for camera location/targeting
        public Vector3 CameraDirection;
        public Vector3 CameraPosition;
        public Vector3 DesiredPosition;
        public Vector3 CameraTarget;
        public Vector3 DesiredTarget;

        // Determine whether the camera is moving to reduce expensive calculations
        public bool IsMoving
        {
            get { return (CameraPosition != DesiredPosition || CameraTarget != DesiredTarget); }
        }

        public SimpleCamera() { }

        /// <summary>
        /// Reset the camera to desired values
        /// </summary>
        /// <param name="p_aspectRatio"></param>
        /// <param name="p_cameraPosition"></param>
        /// <param name="p_cameraTarget"></param>
        public void Create(Vector3 p_cameraPosition, Vector3 p_cameraTarget, Viewport p_viewport, ViewportLocation viewportLocation)
        {
            // Reset the aspects of the camera based on parameters
            CameraPosition = p_cameraPosition;                                   // Reset camera's position
            CameraTarget = p_cameraTarget;                                       // Reset camera's target
            DesiredPosition = p_cameraPosition;
            DesiredTarget = p_cameraTarget;

            CameraDirection = Vector3.Normalize(CameraTarget - CameraPosition);

            // Construct projection and view matrices
            switch (viewportLocation)
            {
                case ViewportLocation.BottomHalf:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    customViewport.Y = customViewport.Height + 1;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.BottomLeft:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    customViewport.Y = customViewport.Height + 1;
                    customViewport.Width = customViewport.Width / 2;
                    customViewport.X = 0;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.BottomRight:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    customViewport.Y = customViewport.Height + 1;
                    customViewport.Width = customViewport.Width / 2;
                    customViewport.X = customViewport.Width + 1;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.FullScreen:
                    customViewport = p_viewport;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.TopHalf:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.TopLeft:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    customViewport.Width = customViewport.Width / 2;
                    customViewport.X = 0;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
                case ViewportLocation.TopRight:
                    customViewport = p_viewport;
                    customViewport.Height = customViewport.Height / 2;
                    customViewport.Width = customViewport.Width / 2;
                    customViewport.X = customViewport.Width + 1;
                    ScreenSize = new Vector2(customViewport.Width, customViewport.Height);
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView / 2, aspectRatio, nearPlaneDistance, farPlaneDistance);
                    break;
            }

            viewMatrix = Matrix.CreateLookAt(CameraPosition, CameraTarget, Vector3.Up);

            viewFrustum = new BoundingFrustum(viewMatrix * projectionMatrix);
        }

        /// <summary>
        /// Hard reset the camera position and target.
        /// </summary>
        /// <param name="p_cameraPosition"></param>
        /// <param name="p_cameraTarget"></param>
        public void Reset(Vector3 p_cameraPosition, Vector3 p_cameraTarget)
        {
            CameraPosition = DesiredPosition = p_cameraPosition;
            CameraTarget = DesiredTarget = p_cameraTarget;
        }

        /// <summary>
        /// Rotate the camera around the lookat location on the YAxis in radians.
        /// </summary>
        /// <param name="RotateAmount">Number of radians to rotate in the YAxis.</param>
        public void RotateAroundLookatY(float RotateAmount)
        {
            Matrix h = Matrix.CreateRotationY(RotateAmount);

            CameraPosition = CameraPosition - CameraTarget;
            CameraPosition = Vector3.Transform(CameraPosition, h);
            CameraPosition = CameraPosition + CameraTarget;

            DesiredPosition = CameraPosition;

            CameraDirection = Vector3.Normalize(CameraTarget - CameraPosition);
        }

        public void Zoom(float zoomAmount)
        {
            CameraDirection = Vector3.Normalize(CameraTarget - CameraPosition);

            // Sharp transition
            if (DesiredPosition.Y + (CameraDirection.Y * zoomAmount) > zoomMin &&
                DesiredPosition.Y + (CameraDirection.Y * zoomAmount) < zoomMax)
            {
                DesiredPosition += (CameraDirection * zoomAmount);
                //CameraPosition = DesiredPosition;   // uncomment for sharp transition
            }
        }

        /// <summary>
        /// Translate the camera by a set amount.
        /// </summary>
        /// <param name="TranslateAmount"></param>
        public void Translate(Vector3 TranslateAmount)
        {
            
            if (BoardVariables.boundingSphere.Contains(CameraTarget + TranslateAmount) == ContainmentType.Contains &&
                BoardVariables.boundingSphere.Contains(DesiredTarget + TranslateAmount) == ContainmentType.Contains)
            {
                CameraPosition += TranslateAmount;
                DesiredPosition += TranslateAmount;

                CameraTarget += TranslateAmount;
                DesiredTarget += TranslateAmount;
            }

        }

        /// <summary>
        /// Update the camera.
        /// </summary>
        /// <param name="gameTime">Elapsed time.</param>
        public void Update(GameTime gameTime)
        {
            // This section replaces the following lines:
            // Vector3.Lerp(ref CameraPosition, ref DesiredPosition, (float)gameTime.ElapsedGameTime.Milliseconds / 500.0f, out CameraPosition);
            // Vector3.Lerp(ref cameraTarget, ref DesiredTarget, (float)gameTime.ElapsedGameTime.Milliseconds / 500.0f, out cameraTarget);
            // The reason is so that the frustum/linked events/etc are calculated only when camera moves,
            // which is sometimes a problem as lerping will often overshoot depending on timestep, etc.
            // Math.Max(...) added because at extremely high framerates, the elapsed milliseconds become 0, freezing the camera.
            if (this.IsMoving)
            {
                lerpValue = (float)gameTime.ElapsedGameTime.Milliseconds / 400.0f;

                // CHECK POSITION
                Vector3.Lerp(ref CameraPosition, ref DesiredPosition, Math.Max(lerpValue, MinLerpValue), out VectorAfterLerp);
                x = Math.Abs(VectorAfterLerp.X - DesiredPosition.X);
                y = Math.Abs(VectorAfterLerp.Y - DesiredPosition.Y);
                z = Math.Abs(VectorAfterLerp.Z - DesiredPosition.Z);
                if (x < ClampValue) CameraPosition.X = DesiredPosition.X; else CameraPosition.X = VectorAfterLerp.X;
                if (y < ClampValue) CameraPosition.Y = DesiredPosition.Y; else CameraPosition.Y = VectorAfterLerp.Y;
                if (z < ClampValue) CameraPosition.Z = DesiredPosition.Z; else CameraPosition.Z = VectorAfterLerp.Z;

                // Added explicity for this size board game in order to increase shadows cheaply
                // On "zoom", it readjusts the projection to increase relative camera resolution
                if (CameraPosition.Y != DesiredPosition.Y)
                    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlaneDistance, CameraPosition.Y + BoardVariables.cameraFarAdjustment);

                // CHECK TARGET
                Vector3.Lerp(ref CameraTarget, ref DesiredTarget, Math.Max(lerpValue, MinLerpValue), out VectorAfterLerp);
                x = Math.Abs(VectorAfterLerp.X - DesiredTarget.X);
                y = Math.Abs(VectorAfterLerp.Y - DesiredTarget.Y);
                z = Math.Abs(VectorAfterLerp.Z - DesiredTarget.Z);
                if (x < ClampValue) CameraTarget.X = DesiredTarget.X; else CameraTarget.X = VectorAfterLerp.X;
                if (y < ClampValue) CameraTarget.Y = DesiredTarget.Y; else CameraTarget.Y = VectorAfterLerp.Y;
                if (z < ClampValue) CameraTarget.Z = DesiredTarget.Z; else CameraTarget.Z = VectorAfterLerp.Z;
            }
            
            viewMatrix = Matrix.CreateLookAt(CameraPosition, CameraTarget, Vector3.Up);

            ViewFrustum.Matrix = viewMatrix * projectionMatrix;
        }
    }
}
